// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//

using System;
using System.Collections.Generic;
using KnessetCore.Members;
using KnessetCore.Members.Views;
using KnessetCore.Utils;
using log4net;

namespace KnessetCore.DataObjectAccess
{
	[System.ComponentModel.DataObject] 
	/// <summary>
	/// Used in order to access data more easily using object data source.
	/// </summary>
	public class TimeAccess
	{
		#region Private variables.
		/// <summary>
		/// Log data.
		/// </summary>
		private static readonly ILog logger = LogManager.GetLogger("KnessetCore.DataObjectAccess");
		#endregion

		#region Public methods

		/// <summary>
		/// Gets a cached version of yesterday's time information. The cached version is static i.e. any changes
		/// to the underline data (even refresh) will not change results after they have been acquired.
		/// </summary>
		/// <param name="sortBy">Sort according to which field. Can accept either Value1, Value2 or Value3 as string values. Can not be null..</param>
		///  <returns>
		/// Holds values for previous 365 days. Each element is composed of:
		/// 1. Member's name.
		/// 2. Total time accumulated for previous 365 days.
		/// 3. Average per day.</returns>
		[System.ComponentModel.DataObjectMethodAttribute(System.ComponentModel.DataObjectMethodType.Select, false)]
		public static ICollection<TimeMemberView> GetYesterday(String sortBy)
		{
			logger.Debug("Entered TimeAccess.GetYesterday");
			// Used for logging.
			String cacheName = "yesterday's";
			List<TimeMemberView> yesterday = new List<TimeMemberView>();
			DateTime yesterdaysDate = DateTime.Today.Subtract(new TimeSpan(1, 0, 0, 0));
			fillData(yesterdaysDate, yesterdaysDate, cacheName, yesterday, sortBy);
			logger.Debug("Exited TimeAccess.GetYesterday");
			// Add asterisk in case member has more than 12 hours.
			addAsterisk(yesterday);
			return yesterday;
		}


		/// <summary>
		/// Gets a cached version of the last 30 days time information. The cached version is static i.e. any changes
		/// to the underline data (even refresh) will not change results after they have been acquired.
		/// </summary>
		/// <param name="sortBy">Sort according to which field. Can accept either Value1, Value2 or Value3 as string values. Can not be null..</param>
		///  <returns>
		/// Holds values for previous 30 days. Each element is composed of:
		/// 1. Member's name.
		/// 2. Total time accumulated for previous 30 days.
		/// 3. Average per day.</returns>
		[System.ComponentModel.DataObjectMethodAttribute(System.ComponentModel.DataObjectMethodType.Select, false)]
		public static ICollection<TimeMemberView> GetPrevious30Days(String sortBy)
		{
			logger.Debug("Entered TimeAccess.GetPrevious30Days");
			// Represents 30 days in time span.
			TimeSpan thirtyDays = new TimeSpan(TimeSpan.TicksPerDay * 30);
			DateTime previous30DaysDate = DateTime.Today.Subtract(thirtyDays);

			List<TimeMemberView> previous30Days = new List<TimeMemberView>();
			fillData(previous30DaysDate, DateTime.Today, "last 30 days's", previous30Days, sortBy);
			logger.Debug("Exited TimeAccess.GetPrevious30Days");
			return previous30Days;
		}

		/// <summary>
		/// Gets a cached version of the last 365 days time information. The cached version is static i.e. any changes
		/// to the underline data (even refresh) will not change results after they have been acquired.
		/// </summary>
		/// <param name="sortBy">Sort according to which field. Can accept either Value1, Value2 or Value3 as string values. Can not be null..</param>
		/// <returns>
		/// Holds values for previous 365 days. Each element is composed of:
		/// 1. Member's name.
		/// 2. Total time accumulated for previous 365 days.
		/// 3. Average per day.</returns>
		[System.ComponentModel.DataObjectMethodAttribute(System.ComponentModel.DataObjectMethodType.Select, false)]
		public static ICollection<TimeMemberView> GetPrevious365Days(String sortBy)
		{
			logger.Debug("Entered TimeAccess.GetPrevious365Days");
			List<TimeMemberView> previous365Days = new List<TimeMemberView>();
			// Represents 365 days in time span.
			TimeSpan yearInDays = new TimeSpan(TimeSpan.TicksPerDay * 365);
			DateTime previous365DaysDate = DateTime.Today.Subtract(yearInDays);
			fillData(previous365DaysDate, DateTime.Today, "last 365 days's", previous365Days, sortBy);
			logger.Debug("Exited TimeAccess.GetPrevious365Days");

			return previous365Days;
		}

		/// <summary>
		/// Gets today's time information. This version is static i.e. any changes
		/// to the underline data will not change results after they have been acquired.
		/// </summary>
		/// <param name="sortBy">Sort according to which field. Can accept either Value1, Value2 or Value3 as string values. Can not be null..</param>
		/// <returns>
		/// Holds values for today. Each element is composed of:
		/// 1. Member's name.
		/// 2. Total time accumulated for today.
		/// 3. Average per day.</returns>
		[System.ComponentModel.DataObjectMethodAttribute(System.ComponentModel.DataObjectMethodType.Select, true)]
		public static ICollection<TimeMemberView> GetToday(String sortBy)
		{
			logger.Debug("Entered TimeAccess.GetToday");
			List<TimeMemberView> today = new List<TimeMemberView>();
			fillData(DateTime.Today, DateTime.Today, "today's", today, sortBy);
			logger.Debug("Exited TimeAccess.GetToday");
			addAsterisk(today);
			return today;
		}

		/// <summary>
		/// Gets time information from and to specific date. This version is static i.e. any changes
		/// to the underline data will not change results after they have been acquired.
		/// </summary>
		/// <param name="from">Run query from this date</param>
		/// <param name="to">Run query up to this date.</param>
		/// <param name="sortBy">Sort according to which field. Can accept either Value1, Value2 or Value3 as string values. Can not be null..</param>
		/// <returns>
		/// Holds values for specific date range. Each element is composed of:
		/// 1. Member's name.
		/// 2. Total time accumulated for today.
		/// 3. Average per day.</returns>
		[System.ComponentModel.DataObjectMethodAttribute(System.ComponentModel.DataObjectMethodType.Select, true)]
		public static ICollection<TimeMemberView> GetFromToDate(DateTime from, DateTime to, String sortBy)
		{
			logger.Debug("Entered TimeAccess.GetFromToDate");
			List<TimeMemberView> fromTo = new List<TimeMemberView>();
			// If from is  > to then ignore query.
			if (from.CompareTo(to) <= 0)
			{
				fillData(from, to, "from to", fromTo, sortBy);
				
			} else
			{
				logger.Info(string.Format("Invalid date range. From: {0} To: {1}", from, to));
			}
			logger.Debug("Exited TimeAccess.GetFromToDate");
			return fromTo;
		}
		#endregion

		#region Private methods
		/// <summary>
		/// Remove milliseconds and seconds from a timespan object.
		/// </summary>
		/// <param name="timeSpan">Remove the milliseconds from this time span.</param>
		/// <returns>The same time span, but the milliseconds part = 0.</returns>
		private static TimeSpan removeMilliseconds(TimeSpan timeSpan)
		{
			TimeSpan returnValue = new TimeSpan(timeSpan.Days, timeSpan.Hours, timeSpan.Minutes, 0);

			return returnValue;
		}

		/// <summary>
		/// Add asterisk to its name in case member has more than valud hours.
		/// </summary>
		/// <param name="membersList">members list to search for.</param>
		private static void addAsterisk(ICollection<TimeMemberView> membersList)
		{
			// If the time span is 12 hours and above then mark the Knesset member with an asterisk.
			foreach (TimeMemberView view in membersList)
			{
				if (view.TotalForTimePeriod.CompareTo(Constants.SUSPECIOUS_OVER_TIME) >= 0)
				{
					view.Name = view.Name + " *";
				}
			}
		}

		/// <summary>
		/// Fill time data retrieved from Hours database.
		/// </summary>
		/// <param name="from">From which time to start.</param>
		/// <param name="to">To which time to end.</param>
		/// <param name="cacheName">Name of the cache which we fill. Used for logging.</param>
		/// <param name="listToFill">Fill this list and sort if needed.</param>
		/// <param name="sortBy">Sort by parameter.</param>
		private static void fillData(DateTime from, DateTime to, string cacheName, List<TimeMemberView> listToFill, string sortBy)
		{
			logger.Debug("Entered TimeAccess.fillData");
			KnessetMembers members = KnessetMembers.GetInstance();
			
			double numberOfDays = to.Subtract(from).TotalDays; // Number of days to calculate.
			numberOfDays++; // Always include the 'to' and 'from' in your number of days.
			foreach (KnessetMember member in members.GetMembers())
			{
				TimeSpan totalTime = member.GetTotalHours(to, from);
				// Get the average per day.
				TimeSpan averagePerDay = new TimeSpan((long) (totalTime.Ticks / numberOfDays));
				// Remove miliseconds from the timespan in order to have a normal view.
				averagePerDay = removeMilliseconds(averagePerDay);
				//todo fix here the empty links.
				TimeMemberView value = new TimeMemberView(member.Name, member.ImgLink, member.KnessetLink, totalTime, averagePerDay);
				logger.Info(string.Format("Added {0} to {1} cache.", value, cacheName));
				listToFill.Add(value);
			}

			// Sort values only if the user requests us to sort..
			if (sortBy != null && sortBy != String.Empty)
			{
				listToFill.Sort(TripletComparer.GetInstance(sortBy));
			} 
			else // If no sort by is used then sort according to name.
			{
				listToFill.Sort(TripletComparer.GetInstance("Value1"));
			}
			logger.Debug("Exited TimeAccess.fillData");
		}

		#endregion

		private class TripletComparer : IComparer<TimeMemberView>
		{
			#region Factory method
			/// <summary>
			/// Holds a pre created TripletComparer for value1.
			/// </summary>
			private static readonly TripletComparer value1 = new TripletComparer("Name", false);
			/// <summary>
			/// Holds a pre created TripletComparer for value1. DESC comparator.
			/// </summary>
			private static readonly TripletComparer value1Desc = new TripletComparer("Name", true);
			/// <summary>
			/// Holds a pre created TripletComparer for value2.
			/// </summary>
			private static readonly TripletComparer value2 = new TripletComparer("PeriodTimeSpan", false);
			/// <summary>
			/// Holds a pre created TripletComparer for value2. DESC comparator.
			/// </summary>
			private static readonly TripletComparer value2Desc = new TripletComparer("PeriodTimeSpan", true);
			/// <summary>
			/// Holds a pre created TripletComparer for value3.
			/// </summary>
			private static readonly TripletComparer value3 = new TripletComparer("AverageTimeSpan", false);
			/// <summary>
			/// Holds a pre created TripletComparer for value3. DESC comparator.
			/// </summary>
			private static readonly TripletComparer value3Desc = new TripletComparer("AverageTimeSpan", true);
			/// <summary>
			/// Retrieve TripletComparers using this factory method.
			/// </summary>
			/// <param name="sortBy">Sort according to which field. Can accept either Name, PeriodTimeSpan or AverageTimeSpan as string values. Can not be null.</param>
			/// <returns>A singleton value of the requested sortBy field.</returns>
			/// <exception cref="ArgumentOutOfRangeException">sortBy does not have a valid value.</exception>
			/// <exception cref="ArgumentNullException">sortBy is null.</exception>
			public static TripletComparer GetInstance(String sortBy)
			{
				// PeriodTimeSpan DESC
				if (sortBy.Equals("Name"))
				{
					return value1;
				}
				else if (sortBy.Equals("Name DESC"))
				{
					return value1Desc;
				}
				else if (sortBy.Equals("PeriodTimeSpan"))
				{
					return value2;
				}
				else if (sortBy.Equals("PeriodTimeSpan DESC"))
				{
					return value2Desc;
				}
				else if (sortBy.Equals("AverageTimeSpan"))
				{
					return value3;
				}
				else if (sortBy.Equals("AverageTimeSpan DESC"))
				{
					return value3Desc;
				}
				else // Wrong value.
				{
					logger.Warn(string.Format("sortBy does not have a valid value: {0}", sortBy));
					// Return default.
					return value1;
				}
			} 
			#endregion

			#region Private variables.
			/// <summary>
			/// Do the compare in order to recieve a descendant sort.
			/// </summary>
			private readonly bool descCompare;
			/// <summary>
			/// Sort according to which field. Can accept either Name, PeriodTimeSpan or AverageTimeSpan as string values.
			/// </summary>
			private readonly SortTimeMemberViewValues sortBy;
			/// <summary>
			/// Available sort types.
			/// </summary>
			private enum SortTimeMemberViewValues
			{
				Name,
				PeriodTimeSpan,
				AverageTimeSpan
			} 
			#endregion


			/// <summary>
			/// Create new object.
			/// </summary>
			/// <param name="sortBy">Sort according to which field. Can accept either Name, PeriodTimeSpan or AverageTimeSpan as string values. Can not be null.</param>
			/// <param name="descCompare">Do the compare in order to recieve a descendant sort.</param>
			/// <exception cref="ArgumentOutOfRangeException">sortBy does not have a valid value.</exception>
			/// <exception cref="ArgumentNullException">sortBy is null.</exception>
			public TripletComparer(string sortBy, bool descCompare)
			{
				this.descCompare = descCompare;
				if (sortBy == null) throw new ArgumentNullException("sortBy");
				
				// Convert to enum. Using enum will make the comparison a little bit faster.
				if (sortBy == "Name")
				{
					this.sortBy = SortTimeMemberViewValues.Name;
				} else if (sortBy == "PeriodTimeSpan")
				{
					this.sortBy = SortTimeMemberViewValues.PeriodTimeSpan;
				}
				else if (sortBy == "AverageTimeSpan")
				{
					this.sortBy = SortTimeMemberViewValues.AverageTimeSpan;
				} else // Wrong value.
				{
					throw new ArgumentOutOfRangeException(string.Format("sortBy does not have a valid value: {0}", sortBy));
				}
			}

			#region IComparer<Triplet<string,TimeSpan,TimeSpan>> Members

			///<summary>
			///Compares two objects and returns a value indicating whether one is less than, equal to, or greater than the other.
			///</summary>
			///
			///<returns>
			///Value Condition Less than zero x is less than y.Zero x equals y.Greater than zero x is greater than y.
			///</returns>
			///
			///<param name="y">The second object to compare.</param>
			///<param name="x">The first object to compare.</param>
			public int Compare(TimeMemberView x, TimeMemberView y)
			{
				switch(sortBy)
				{
					case SortTimeMemberViewValues.Name:
						// If we want a descendent compare then return the opposite.
						return descCompare ? 0 - x.Name.CompareTo(y.Name) : x.Name.CompareTo(y.Name);
					case SortTimeMemberViewValues.PeriodTimeSpan:
						return descCompare ? 0 - x.TotalForTimePeriod.CompareTo(y.TotalForTimePeriod) : x.TotalForTimePeriod.CompareTo(y.TotalForTimePeriod);
					case SortTimeMemberViewValues.AverageTimeSpan:
						return descCompare ? 0 - x.AverageForTimePeriod.CompareTo(y.AverageForTimePeriod) : x.AverageForTimePeriod.CompareTo(y.AverageForTimePeriod);
					default:
						throw new ArgumentOutOfRangeException();
				}
			}

			#endregion
		}
	}
}